#!/usr/bin/env python
import sys
from math import cos, sin, pi, hypot

import pygame

import primitives
import blocks
import render
from level import Level
from geometry import getDirection, pointOnSegment, pointOnCircle

UNIT = 20

MODE_PLAN = 0
MODE_OBJECT = 1

PLAN_TYPE_LINE = 0
PLAN_TYPE_CIRCLE = 1

PLACE_TYPE_RECTANGLE = 0
PLACE_TYPE_CIRCLE = 1
PLACE_TYPE_ARC = 2

#Graphics constants
COLOR0 = 255, 255, 255
COLOR1 = 255, 127, 0
COLOR2 = 255, 0, 0
COLOR3 = 0, 0, 0
COLOR4 = 191, 191, 191

class Point(primitives.Circle):
    image = render.silhouette(primitives.Circle(2.0, 2.0, 2.0), COLOR4, True)
    hitImage = render.silhouette(primitives.Circle(2.0, 2.0, 2.0), COLOR2, True)
    def __init__(self, x, y, direction=None, center=None):
        primitives.Circle.__init__(self, x, y, 2.0)
        self.x = x
        self.y = y
        
        self.direction = direction
        self.center = center
        
        self.rect = pygame.Rect(self.getRectangle())
    
    def draw(self, display):
        display.blit(self.image, self.rect)

class Line(primitives.Capsule):
    def __init__(self, x0, y0, x1, y1, radius=2.0):
        primitives.Capsule.__init__(self, x0, y0, x1, y0, radius)
        
        self.pointNum = 2.0
        self.length = 0.0
        self.segmentLength = self.length / self.pointNum
        
        self.rect = pygame.Rect(0,0,0,0)
        self.points = set()
        
        self.image = None
        self.hitImage = None
        
        self.refresh()
    
    def refresh(self, unit=UNIT):
        dx = self.x1 - self.x0
        dy = self.y1 - self.y0
        length = hypot(dx, dy)
        if length:
            self.x1 = self.x0 + dx / length * self.pointNum * unit
            self.y1 = self.y0 + dy / length * self.pointNum * unit
            dx = self.x1 - self.x0
            dy = self.y1 - self.y0
            self.length = hypot(dx, dy)
            self.segmentLength = self.length / self.pointNum
            
            self.rect = pygame.Rect(self.getRectangle())
            
            self.points.clear()
            for i in range(int(self.pointNum + 1)):
                x = self.x0 + dx * (i / self.pointNum)
                y = self.y0 + dy * (i / self.pointNum)
                self.points.add(Point(x, y, dir, (self.x0, self.y0)))
    
    def snap(self, x, y):
        distance = hypot(x - self.x0, y - self.y0)
        decrement = distance % self.segmentLength
        if decrement < self.segmentLength / 2.0:
            snapDistance = distance - decrement
        else:
            snapDistance = distance - decrement + self.segmentLength
        x = self.x0 + (self.x1 - self.x0) / self.length * snapDistance
        y = self.y0 + (self.y1 - self.y0) / self.length * snapDistance
        return x, y
    
    def finalize(self):
        self.rect.normalize()
        self.image = render.silhouette(self, COLOR4, True)
        self.hitImage = render.silhouette(self, COLOR2, True)
    
    def draw(self, display):
        if self.image:
            display.blit(self.image, self.rect)
        else:
            pygame.draw.line(display, COLOR4, (self.x0, self.y0), (self.x1, self.y1), 1)
            for point in self.points:
                point.draw(display)

class Circle(primitives.Donut):
    def __init__(self, x, y):
        primitives.Donut.__init__(self, x, y, 1, 2)
        
        self.radius = 0.0
        self.direction = 0.0
        self.pointNum = 2.0
        
        self.rect = pygame.Rect(0,0,0,0)
        self.points = set()
        
        self.image = None
        self.hitImage = None
        
        self.refresh()
    
    def refresh(self, unit=UNIT):
        self.radius = unit * self.pointNum / pi / 2.0
        self.outRadius = self.radius + 2.0
        self.inRadius = self.radius - 2.0
        
        self.rect = pygame.Rect(*self.getRectangle())
        
        self.points.clear()
        for i in range(self.pointNum):
            direction = self.direction + (i / self.pointNum) * pi * 2
            x = self.x + cos(direction) * self.outRadius
            y = self.y + sin(direction) * self.outRadius
            self.points.add(Point(x, y, direction, (self.x, self.y)))
    
    def snap(self, x, y):
        direction0 = getDirection(self.x, self.y, x, y)
        direction1 = direction0 - direction0 % (pi * 2 / self.pointNum)
        x = self.x + cos(direction1) * self.radius
        y = self.y + sin(direction1) * self.radius
        return x, y
    
    def finalize(self):
        self.rect.normalize()
        self.image = render.silhouette(self, COLOR4, True)
        self.hitImage = render.silhouette(self, COLOR2, True)
    
    def draw(self, display):
        if self.image:
            display.blit(self.image, self.rect)
        else:
            pygame.draw.circle(display, COLOR4, (self.x, self.y), self.outRadius, 1)
            for point in self.points:
                point.draw(display)

class PlanMode:
    def __init__(self, editor):
        self.editor = editor
        
        self.font = pygame.font.Font(None, 12)
        
        self.mode = 0
        self.step = 0
        self.preview = None
        
        self.mx = 0
        self.my = 0
        self.mb = 0 #mouse button
    
    def keyPress(self, key):
        if key == pygame.K_F4:
            self.mode = PLAN_TYPE_LINE
            self.preview = None
        elif key == pygame.K_F5:
            self.mode = PLAN_TYPE_CIRCLE
            self.preview = None
    
    def mouseMove(self, mx, my):
        self.mx = mx
        self.my = my 
        self.edit()
    
    def mousePress(self, button):
        self.mb = button
        
        if button == 1:
            self.place()
        elif button == 3:
            self.remove()
        elif button == 4:
            if self.preview:
                self.preview.pointNum += 1
                self.preview.refresh()
        elif button == 5:
            if self.preview and self.preview.pointNum > 1:
                self.preview.pointNum -= 1
                self.preview.refresh()
    
    def place(self):
        placePoints = False
        
        if self.mode == PLAN_TYPE_LINE:
            if self.step == 0:
                self.preview = Line(self.mx, self.my, self.mx, self.my)
                self.step = 1
            elif self.step == 1:
                self.preview.finalize()
                self.editor.level.plans.add(self.preview)
                self.editor.level.planTree.insert(self.preview)
                placePoints = True
                self.preview = None
                self.step = 0
        
        elif self.mode == PLAN_TYPE_CIRCLE:
            if self.step == 0:
                self.preview = Circle(self.mx, self.my)
                point = Point(self.mx, self.my)
                self.editor.level.plans.add(point)
                self.editor.level.planTree.insert(point)
                self.step = 1
            elif self.step == 1:
                self.preview.finalize()
                self.editor.level.plans.add(self.preview)
                self.editor.level.planTree.insert(self.preview)
                placePoints = True
                self.preview = None
                self.step = 0
        
        if False:
            for point in self.preview.points:
                    self.editor.level.plans.add(point)
                    self.editor.level.planTree.insert(point)
    
    def edit(self):
        if self.preview:
            if self.mode == PLAN_TYPE_LINE:
                self.preview.x1 = self.mx
                self.preview.y1 = self.my
            elif self.mode == PLAN_TYPE_CIRCLE:
                self.preview.direction = getDirection(self.preview.x, self.preview.y,
                                                      self.mx, self.my)
            self.preview.refresh()
    
    def remove(self):
        if self.step == 0:
            hits = self.editor.level.planTree.hit(pygame.Rect(self.mx-1, self.my-1, 2, 2))
            for plan in hits:
                if plan.collidePoint(self.mx, self.my):
                    self.editor.level.plans.remove(plan)
                    self.editor.level.planTree.remove(plan)
                    break
        else:
            self.preview = None
            self.step = 0
    
    def draw(self, display):
        if self.preview:
            self.preview.draw(display)
            #pointNum label
            image = self.font.render(str(self.preview.pointNum), True, COLOR3)
            x, y = self.font.size(str(self.preview.pointNum))
            display.blit(image, (self.mx - x, self.my - y))

class ObjectMode:
    def __init__(self, editor):
        self.editor = editor
        
        self.mode = 0
        
        self.mx = 0
        self.my = 0
        self.mb = 0
    
    def keyPress(self, key):
        if key == pygame.K_F4:
            self.mode = PLACE_TYPE_RECTANGLE
        elif key == pygame.K_F5:
            self.mode = PLACE_TYPE_CIRCLE
        elif key == pygame.K_F6:
            self.mode = PLACE_TYPE_ARC
    
    def place(self):
        if self.editor.hoveredPlanTouched:
            item = self.placeTypes[self.placeType](*self.placeArguments[self.placeType])
            self.level.quadTree.insert(item)
            self.items[self.placeType].append(item)
            return item
    
    def remove(self):
        hits = self.level.quadTree.hit(pygame.Rect(self.mx-2, self.my-2, 4, 4))
        for block in hits:
            if block.collidePoint(self.mx, self.my):
                self.level.blocks.remove(block)
                self.level.quadTree.remove(block)
                break #Remove only one item at a time
    
    def draw(self, display):
        pass

class Editor:
    def __init__(self):
        self.level = Level()
        
        self.mode = MODE_PLAN
        self.modes = [PlanMode(self),
                      ObjectMode(self)]
        
        self.mx = 0 #Mouse position
        self.my = 0
        
        self.preferenceValues = {Point: 0, Line: 1, Circle: 2}
        
        self.hoveredPlan = None
        self.hoveredPlanPx = 0.0 #Projection position
        self.hoveredPlanPy = 0.0
        self.hoveredPlanTouched = False
    
    def getHoveredPlan(self):
        hits = self.level.planTree.hit(pygame.Rect(self.mx-2, self.my-2, 4, 4))
        if hits:
            candidates = list((self.preferenceValues[plan.__class__], plan) for plan in hits if plan.collidePoint(self.mx, self.my))
            if candidates:
                candidates.sort()
                plan = candidates[0][1]
                if plan.__class__ == Point:
                    x, y = plan.x, plan.y
                elif plan.__class__ == Line:
                    x, y = pointOnSegment(self.mx, self.my, plan.x0, plan.y0, plan.x1, plan.y1)
                elif plan.__class__ == Circle:
                    x, y = pointOnCircle(self.mx, self.my, plan.x, plan.y, plan.radius)
                self.hoveredPlan = plan
                self.hoveredPlanPx = x
                self.hoveredPlanPy = y
                self.hoveredPlanTouched = True
            else:
                self.hoveredPlanTouched = False
        else:
            self.hoveredPlanTouched = False
    
    def update(self, events):
        #Handle events
        for event in events:
            if event.type == pygame.MOUSEMOTION:
                self.mx, self.my = pygame.mouse.get_pos()
                self.getHoveredPlan()
                if self.hoveredPlanTouched:
                    self.mx, self.my = self.hoveredPlan.snap(self.mx, self.my)
                    #self.mx = self.hoveredPlanPx
                    #self.my = self.hoveredPlanPy
                self.modes[self.mode].mouseMove(self.mx, self.my)
            elif event.type == pygame.MOUSEBUTTONDOWN:
                self.modes[self.mode].mousePress(event.button)
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_F1:
                    self.mode = MODE_PLAN
                elif event.key == pygame.K_F2:
                    self.mode = MODE_OBJECT
                else:
                    self.modes[self.mode].keyPress(event.key)
            elif event.type == pygame.QUIT:
                self.back()
    
    def draw(self, display):
        display.fill(COLOR0)
        
        for block in self.level.blocks:
            block.draw(display)
        for plan in self.level.plans:
            plan.draw(display)
        
        self.modes[self.mode].draw(display)
        if self.hoveredPlanTouched:
            pygame.draw.circle(display, COLOR1, (self.mx, self.my), 2)